﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Web;

namespace WoWAPI
{
	public class API
	{
		#region Application Members
		public static bool QueueingEnabled = false;
		public static int DailyLimit = 3000;
		public static string ApplicationPublicKey;
		public static string ApplicationSignature;
		private static APIResponseCollection _ResponseCache = new APIResponseCollection();
		#endregion

		public static string MakeWebRequest(string sWebAddress)
		{
			return MakeWebRequest(sWebAddress, null);
		}

		public static string MakeWebRequest(string webAddress, DateTime? LastModified)
		{
			try
			{
				Uri uri;
				CustomWebRequest request;
				if (!String.IsNullOrEmpty(ApplicationPublicKey))
				{
					uri = new Uri(webAddress.Replace("http://", "https://"));
					//uri = new Uri(webAddress);
					request = new CustomWebRequest(uri);
					
					string requestDate = DateTime.Now.ToUniversalTime().ToString("R");
					request.Headers.Add("Date", requestDate);

					string data = "GET\n" + requestDate + "\n" + uri.LocalPath + "\n";

					//sign the dang thing
					System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
					byte[] keyBytes = encoding.GetBytes(ApplicationSignature);
					HMACSHA1 hash = new HMACSHA1(keyBytes);
					byte[] dataBytes = encoding.GetBytes(data);
					data = System.Convert.ToBase64String(hash.ComputeHash(dataBytes));

					request.Headers.Add("Authorization", "BNET " + ApplicationPublicKey + ":" + HttpUtility.UrlPathEncode(data));
				}
				else
				{
					uri = new Uri(webAddress);
					request = new CustomWebRequest(uri);
				}

				//Add Last-Modified headers if we have the request in cache
				if (_ResponseCache.ContainsURL(webAddress))
					request.Headers.Add("If-Modified-Since", _ResponseCache[webAddress].Date.ToString("R")); 

				if (LastModified.HasValue)
				{
					request.Headers.Add("If-Modified-Since", LastModified.Value.ToString("R")); 
				}

				string responseBody = request.GetResponse();

				//cache response
				APIResponse responseCacheItem = new APIResponse();
				responseCacheItem.Date = DateTime.Now;
				responseCacheItem.ResponseBody = responseBody;
				responseCacheItem.URL = webAddress;
				_ResponseCache.Add(responseCacheItem);

				return responseBody;

			}
			catch (CustomWebException ex)
			{
				Exception wowException = new Exception();
				switch (ex.ResponseStatus)
				{
					case HttpStatusCode.InternalServerError:
						WoWCommunityAPIException responseError = DeserializeJSON<WoWCommunityAPIException>(ex.ResponseBody);
						if (!String.IsNullOrEmpty(responseError.Status))
						{
							wowException = new Exception(String.Format(
@"URL: {0} returned the following error:
Response code: 500
Status: {1}
Reason: {2}
Details: {3}", webAddress, responseError.Status, responseError.Reason, responseError.Details), ex);
							wowException.Data.Add("WoWError", responseError);
						}
						else
							wowException = new Exception(String.Format("URL: {0} returned the following error: Response code 500 - \"{1}\"", webAddress, ex.ResponseBody), ex);
						break;
					case HttpStatusCode.NotModified: //we passed in a "Last-Modified" header and they're telling us it hasn't been modified sine then.
						return _ResponseCache[webAddress].ResponseBody;
					default:
						wowException = new Exception(String.Format("URL: {0} returned the following error: Response code {1} - \"{2}\"", webAddress, ex.ResponseStatus, ex.ResponseBody), ex);
						break;
				}
				throw wowException;
			}
		}

		public static T DeserializeJSON<T>(string json)
		{			
			System.Web.Script.Serialization.JavaScriptSerializer jss = new System.Web.Script.Serialization.JavaScriptSerializer();
			return jss.Deserialize<T>(json);
		}

		private static string GetResponseText(Stream responseStream)
		{
			StreamReader objReader = new StreamReader(responseStream);
			StringBuilder sbResponse = new StringBuilder();
			return objReader.ReadToEnd();
		}

		[Serializable]
		private class WoWCommunityAPIException
		{
			public string Status { get; set; }
			public string Reason { get; set; }
			/// <summary>
			/// This property returns the more detailed explanation for the error from the battle.net api documentation.
			/// </summary>
			public string Details
			{
				get
				{
					switch (Reason)
					{
						case "Invalid Application":
							return "A request was made including application identification information, but either the application key is invalid or missing.";
						case "Invalid application permissions.":
							return "A request was made to an API resource that requires a higher application permission level.";
						case "Access denied, please contact api-support@blizzard.com":
							return "The application or IP address has been blocked from making further requests. This ban may not be permanent.";
						case "When in doubt, blow it up. (page not found)":
							return "A request was made to a resource that doesn't exist.";
						case "If at first you don't succeed, blow it up again. (too many requests)":
							return "The application or IP has been throttled.";
						case "Have you not been through enough? Will you continue to fight what you cannot defeat? (something unexpected happened)":
							return "There was a server error or equally catastrophic exception preventing the request from being fulfilled.";
						case "Invalid authentication header.":
							return "The application authorization information was mallformed or missing when expected.";
						case "Invalid application signature.":
							return "The application request signature was missing or invalid. This will also be thrown if the request date outside of a 15 second window from the current GMT time.";
						default:
							return String.Empty;
					}
				}
			}

			public WoWCommunityAPIException() { }
		}

		private class APIResponse
		{
			public DateTime Date { get; set; }
			public string URL { get; set; }
			public string ResponseBody { get; set; }

			public APIResponse() { }
		}

		private class APIResponseCollection
		{
			private List<APIResponse> _list = new List<APIResponse>();
			public int IndexOf(APIResponse item)
			{
				return _list.IndexOf(item);
			}

			public void Insert(int index, APIResponse item)
			{
				_list.Insert(index, item);
			}

			public void RemoveAt(int index)
			{
				_list.RemoveAt(index);
			}

			public APIResponse this[int index]
			{
				get
				{
					return _list[index];
				}
				set
				{
					_list[index] = value;
				}
			}

			public APIResponse this[string URL]
			{
				get
				{
					foreach (APIResponse item in _list)
					{
						if (item.URL == URL)
						{
							return item;
						}
					}

					return null;
				}
				set
				{
					APIResponse specificItem = null;
					foreach (APIResponse item in _list)
					{
						if (item.URL == URL)
						{
							specificItem = value;
							return;
						}
					}
					throw new Exception("APIResponse not found with that URL");
				}
			}

			public void Add(APIResponse item)
			{
				_list.Add(item);
			}

			public void Clear()
			{
				_list.Clear();
			}

			public bool Contains(APIResponse item)
			{
				return _list.Contains(item);
			}

			public bool ContainsURL(string url)
			{
				foreach (APIResponse item in _list)
				{
					if (item.URL == url)
					{
						return true;
					}
				}
				return false;
			}

			public void CopyTo(APIResponse[] array, int arrayIndex)
			{
				_list.CopyTo(array, arrayIndex);
			}

			public int Count
			{
				get { return _list.Count; }
			}

			public bool IsReadOnly
			{
				get { return false; }
			}

			public bool Remove(APIResponse item)
			{
				return _list.Remove(item);
			}

			public IEnumerator<APIResponse> GetEnumerator()
			{
				return _list.GetEnumerator();
			}
		}
	}
}
